{
  "cells": [
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "CvliHAuTCOXC"
      },
      "source": [
        "# Subclass Distillation Demo Colab\n",
        "\n",
        "This Colab trains a convolutional network on MNIST collapsed to 2 classes (0-4 and 5-9) with 10 subclasses, and then distills into a narrow fully connected network.\n",
        "\n",
        "---\n",
        "\n",
        "Copyright 2021 Google LLC\n",
        "\n",
        "Licensed under the Apache License, Version 2.0 (the \"License\");\n",
        "you may not use this file except in compliance with the License.\n",
        "You may obtain a copy of the License at\n",
        "\n",
        "    https://www.apache.org/licenses/LICENSE-2.0\n",
        "\n",
        "Unless required by applicable law or agreed to in writing, software\n",
        "distributed under the License is distributed on an \"AS IS\" BASIS,\n",
        "WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n",
        "See the License for the specific language governing permissions and\n",
        "limitations under the License.\n",
        "\n",
        "Please cite as:\n",
        "\n",
        "    @article{muller2020subclass,\n",
        "      title={Subclass distillation},\n",
        "      author={M{\\\"u}ller, Rafael and Kornblith, Simon and Hinton, Geoffrey},\n",
        "      journal={arXiv preprint arXiv:2002.03936},\n",
        "      year={2020}\n",
        "    }"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "MgwqLLdO7ucT"
      },
      "outputs": [],
      "source": [
        "import tensorflow.compat.v2 as tf\n",
        "import tensorflow_datasets as tfds\n",
        "from typing import Union\n",
        "\n",
        "tf.enable_v2_behavior()\n",
        "\n",
        "builder = tfds.builder('mnist')\n",
        "BATCH_SIZE = 256\n",
        "NUM_EPOCHS = 12\n",
        "NUM_CLASSES = 2  # 2 total classes.\n",
        "NUM_SUBCLASSES = 10  # 10 total subclasses (5 subclasses/class)."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "ESq6w6UxgB0r"
      },
      "source": [
        "## Define Subclass Losses"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "E9acm-rggErH"
      },
      "outputs": [],
      "source": [
        "def class_logits_from_subclass_logits(\n",
        "    subclass_logits: tf.Tensor, num_classes: int) -\u003e tf.Tensor:\n",
        "  \"\"\"Compute class logits from subclass logits.\n",
        "\n",
        "  This function computes `logsumexp` over the subclasses corresponding to each\n",
        "  dataset class to yield class logits. We work in log space for numerical\n",
        "  stability, but `softmax(class_logits)` yields the same probabilities as\n",
        "  summing `softmax(subclass_logits)` over the subclasses corresponding to eac\n",
        "  dataset class.\n",
        "\n",
        "  Args:\n",
        "    subclass_logits: A Tensor of shape `[batch_size, num_subclasses]` of\n",
        "      subclass logits. `num_subclasses` must be a multiple of `num_classes`.\n",
        "    num_classes: Number of output classes.\n",
        "\n",
        "  Returns:\n",
        "    A Tensor of shape `[batch_size]` containing per-example losses.\n",
        "  \"\"\"\n",
        "  # Check arguments.\n",
        "  if subclass_logits.shape.rank != 2:\n",
        "    raise ValueError('`subclass_logits` must be a Tensor of rank 2.')\n",
        "  num_subclasses = int(subclass_logits.shape[-1])\n",
        "  if num_subclasses % num_classes != 0:\n",
        "    raise ValueError(\n",
        "        'Number of subclasses in `subclass_logits` must be a multiple of '\n",
        "        'number of classes in `labels`.')\n",
        "\n",
        "  # Compute cross entropy loss after performing LogSumExp over subclass logits\n",
        "  # corresponding to each class. This is equivalent to summing their\n",
        "  # probabilities but maintains the stability advantages of working with logits.\n",
        "  subclass_logits = tf.reshape(\n",
        "      subclass_logits, (-1, num_classes, num_subclasses // num_classes))\n",
        "  return tf.math.reduce_logsumexp(subclass_logits, -1)\n",
        "\n",
        "\n",
        "def negative_sampling_loss(\n",
        "    subclass_logits: tf.Tensor, temperature: Union[float, tf.Tensor],\n",
        "    device_context=None, epsilon: Union[float, tf.Tensor] = 1e-9):\n",
        "  \"\"\"Negative sampling loss for subclass distillation.\n",
        "\n",
        "  This auxiliary loss is applied to the teacher to encourage the model to use\n",
        "  all subclasses. It penalizes high correlation between the logits in a given\n",
        "  minibatch.\n",
        "\n",
        "  Args:\n",
        "    subclass_logits: A Tensor of shape `[batch_size, num_subclasses]` of\n",
        "      subclass logits.\n",
        "    temperature: Temperature of negative sampling loss. Usually less than 1.\n",
        "    device_context: Instance of `TPUContext` or `None`. If provided, the\n",
        "      negative sampling loss will be computed using logits from all TPU\n",
        "      replicas. When using TPUEstimator, the device context is provided to the\n",
        "      `model_fn` as `params['context']`.\n",
        "    epsilon: Small float used to avoid dividing by 0.\n",
        "\n",
        "  Returns:\n",
        "    A scalar Tensor containing the overall loss.\n",
        "  \"\"\"\n",
        "  if subclass_logits.shape.rank != 2:\n",
        "    raise ValueError('`subclass_logits` must be a Tensor of rank 2.')\n",
        "  temperature = tf.convert_to_tensor(temperature, dtype=subclass_logits.dtype)\n",
        "  epsilon = tf.convert_to_tensor(epsilon, dtype=subclass_logits.dtype)\n",
        "  batch_size = tf.cast(tf.shape(subclass_logits)[0], subclass_logits.dtype)\n",
        "  num_subclasses = tf.convert_to_tensor(int(subclass_logits.shape[-1]),\n",
        "                                        dtype=subclass_logits.dtype)\n",
        "\n",
        "  # Subtract per-example (not batch) mean and variance from subclass logits.\n",
        "  mean, variance = tf.nn.moments(subclass_logits, -1, keepdims=True)\n",
        "  normalized_logits = tf.nn.batch_normalization(\n",
        "      subclass_logits,\n",
        "      mean=mean,\n",
        "      variance=variance,\n",
        "      offset=None,\n",
        "      scale=1. / tf.sqrt(temperature * num_subclasses),\n",
        "      variance_epsilon=epsilon)\n",
        "\n",
        "  scaled_correlations = tf.linalg.matmul(\n",
        "      normalized_logits, normalized_logits, transpose_b=True)\n",
        "  return (tf.math.reduce_mean(tf.math.reduce_logsumexp(scaled_correlations, -1))\n",
        "          - 1 / temperature - tf.math.log(batch_size))\n",
        "\n",
        "\n",
        "def distillation_loss(teacher_logits: tf.Tensor, student_logits: tf.Tensor,\n",
        "                      temperature: Union[float, tf.Tensor]):\n",
        "  \"\"\"Compute distillation loss.\n",
        "\n",
        "  This function computes cross entropy between softened logits and softened\n",
        "  targets. The resulting loss is scaled by the squared temperature so that\n",
        "  the gradient magnitude remains approximately constant as the temperature is\n",
        "  changed. For reference, see Hinton et al., 2014, \"Distilling the knowledge in\n",
        "  a neural network.\"\n",
        "\n",
        "  Args:\n",
        "    teacher_logits: A Tensor of logits provided by the teacher.\n",
        "    student_logits: A Tensor of logits provided by the student, of the same\n",
        "      shape as `teacher_logits`.\n",
        "    temperature: Temperature to use for distillation.\n",
        "\n",
        "  Returns:\n",
        "    A scalar Tensor containing the overall loss.\n",
        "  \"\"\"\n",
        "  soft_targets = tf.nn.softmax(\n",
        "      tf.stop_gradient(teacher_logits) / temperature, -1)\n",
        "  return tf.reduce_mean(\n",
        "      tf.nn.softmax_cross_entropy_with_logits(\n",
        "          soft_targets, student_logits / temperature)) * temperature ** 2"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "QE7ImzZvVltG"
      },
      "source": [
        "## Data Loading and Model Construction"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "Qpsirk4jADDQ"
      },
      "outputs": [],
      "source": [
        "# Load train and test splits.\n",
        "def preprocess(x):\n",
        "  image = tf.image.convert_image_dtype(x['image'], tf.float32)\n",
        "  labels = tf.one_hot(x['label'], builder.info.features['label'].num_classes)\n",
        "  labels = tf.reduce_sum(\n",
        "      tf.reshape(labels, (NUM_CLASSES, NUM_SUBCLASSES // NUM_CLASSES)), -1)\n",
        "  return image, labels\n",
        "\n",
        "\n",
        "mnist_train = tfds.load('mnist', split='train', shuffle_files=False).cache()\n",
        "mnist_train = mnist_train.map(preprocess)\n",
        "mnist_train = mnist_train.shuffle(builder.info.splits['train'].num_examples)\n",
        "mnist_train = mnist_train.batch(BATCH_SIZE, drop_remainder=True)\n",
        "\n",
        "mnist_test = tfds.load('mnist', split='test').cache()\n",
        "mnist_test = mnist_test.map(preprocess).batch(BATCH_SIZE)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "LLOvfznQVlJi"
      },
      "outputs": [],
      "source": [
        "@tf.function\n",
        "def compute_num_correct(model, images, labels):\n",
        "  \"\"\"Compute number of correctly classified images in a batch.\n",
        "\n",
        "  Args:\n",
        "    model: Instance of tf.keras.Model.\n",
        "    images: Tensor representing a batch of images.\n",
        "    labels: Tensor representing a batch of labels.\n",
        "\n",
        "  Returns:\n",
        "    Number of correctly classified images.\n",
        "  \"\"\"\n",
        "  subclass_logits = model(images, training=False)\n",
        "  class_logits = class_logits_from_subclass_logits(\n",
        "      subclass_logits, NUM_CLASSES)\n",
        "  return tf.reduce_sum(\n",
        "      tf.cast(tf.math.equal(tf.argmax(class_logits, -1), tf.argmax(labels, -1)),\n",
        "              tf.float32))"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "kcNtIqeoVhrM"
      },
      "source": [
        "## Teacher Training"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "executionInfo": {
          "elapsed": 1,
          "status": "ok",
          "timestamp": 1629232179039,
          "user": {
            "displayName": "",
            "photoUrl": "",
            "userId": ""
          },
          "user_tz": 240
        },
        "id": "HugaLMMzVgBJ",
        "outputId": "b227ade0-e41e-455f-b07a-3d42ebbf2de4"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Epoch 1: 97.13%\n",
            "Epoch 2: 98.28%\n",
            "Epoch 3: 98.64%\n",
            "Epoch 4: 98.85%\n",
            "Epoch 5: 98.86%\n",
            "Epoch 6: 98.94%\n",
            "Epoch 7: 99.28%\n",
            "Epoch 8: 99.28%\n",
            "Epoch 9: 99.22%\n",
            "Epoch 10: 99.25%\n",
            "Epoch 11: 99.38%\n",
            "Epoch 12: 99.35%\n"
          ]
        }
      ],
      "source": [
        "#@test {\"output\": \"ignore\"}\n",
        "\n",
        "# Hyperparameters for teacher training.\n",
        "BETA = 1.0\n",
        "NEGATIVE_SAMPLING_TEMPERATURE = 1.0\n",
        "\n",
        "# Build CNN teacher.\n",
        "cnn_model = tf.keras.Sequential()\n",
        "cnn_model.add(tf.keras.layers.Conv2D(32, kernel_size=(3, 3), activation='relu'))\n",
        "cnn_model.add(tf.keras.layers.MaxPooling2D(pool_size=(2, 2)))\n",
        "cnn_model.add(tf.keras.layers.Conv2D(64, (3, 3), activation='relu'))\n",
        "cnn_model.add(tf.keras.layers.MaxPooling2D(pool_size=(2, 2), strides=2))\n",
        "cnn_model.add(tf.keras.layers.Flatten())\n",
        "cnn_model.add(tf.keras.layers.Dropout(0.5))\n",
        "cnn_model.add(tf.keras.layers.Dense(128, activation='relu'))\n",
        "cnn_model.add(tf.keras.layers.Dropout(0.5))\n",
        "cnn_model.add(tf.keras.layers.Dense(NUM_SUBCLASSES))\n",
        "\n",
        "\n",
        "@tf.function\n",
        "def compute_teacher_loss(images, labels):\n",
        "  \"\"\"Compute subclass distillation teacher loss for given images and labels.\n",
        "\n",
        "  Args:\n",
        "    images: Tensor representing a batch of images.\n",
        "    labels: Tensor representing a batch of labels.\n",
        "\n",
        "  Returns:\n",
        "    Scalar loss Tensor.\n",
        "  \"\"\"\n",
        "  subclass_logits = cnn_model(images, training=True)\n",
        "\n",
        "  # Compute cross-entropy loss for superclasses.\n",
        "  class_logits = class_logits_from_subclass_logits(\n",
        "      subclass_logits, NUM_CLASSES)\n",
        "  cross_entropy_loss_value = tf.reduce_mean(\n",
        "      tf.nn.softmax_cross_entropy_with_logits(labels, class_logits))\n",
        "\n",
        "  # Compute negative sampling loss to distribute logits.\n",
        "  negative_sampling_loss_value = negative_sampling_loss(\n",
        "      subclass_logits, NEGATIVE_SAMPLING_TEMPERATURE)\n",
        "\n",
        "  return cross_entropy_loss_value + BETA * negative_sampling_loss_value\n",
        "\n",
        "\n",
        "def train_and_evaluate(model, compute_loss_fn):\n",
        "  \"\"\"Perform training and evaluation for a given model.\n",
        "\n",
        "  Args:\n",
        "    model: Instance of tf.keras.Model.\n",
        "    compute_loss_fn: A function that computes the training loss given the\n",
        "      images, and labels.\n",
        "  \"\"\"\n",
        "  optimizer = tf.keras.optimizers.Adam()\n",
        "  for epoch in range(1, NUM_EPOCHS + 1):\n",
        "    # Run training.\n",
        "    print('Epoch {}: '.format(epoch), end='')\n",
        "    for images, labels in mnist_train:\n",
        "      with tf.GradientTape() as tape:\n",
        "        loss_value = compute_loss_fn(images, labels)\n",
        "\n",
        "      grads = tape.gradient(loss_value, model.trainable_variables)\n",
        "      optimizer.apply_gradients(zip(grads, model.trainable_variables))\n",
        "\n",
        "    # Run evaluation.\n",
        "    num_correct = 0\n",
        "    num_total = builder.info.splits['test'].num_examples\n",
        "    for images, labels in mnist_test:\n",
        "      num_correct += compute_num_correct(model, images, labels)\n",
        "\n",
        "    print('{:.2f}%'.format(float(num_correct) / num_total * 100))\n",
        "\n",
        "\n",
        "train_and_evaluate(cnn_model, compute_teacher_loss)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "DdpFSQU4CUiW"
      },
      "source": [
        "### Subclasses\n",
        "\n",
        "Each column in the figure corresponds to a learned subclass. While not perfect, the digits roughly organize by the class in the original dataset."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "colab": {
          "height": 584
        },
        "executionInfo": {
          "elapsed": 944,
          "status": "ok",
          "timestamp": 1629144104187,
          "user": {
            "displayName": "",
            "photoUrl": "",
            "userId": ""
          },
          "user_tz": 240
        },
        "id": "yFacCsxEAxXg",
        "outputId": "5d124efb-6737-4191-ac8c-e5813121ad0d"
      },
      "outputs": [
        {
          "data": {
            "text/plain": [
              "(-0.5, 279.5, 167.5, -0.5)"
            ]
          },
          "execution_count": 6,
          "metadata": {
            "tags": []
          },
          "output_type": "execute_result"
        },
        {
          "data": {
            "image/png": "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\nse+//z4Iowkd9iVdihcvLnn48OGS9TzwxIkTS27RooXk999/3/JYelmIn3/+WfLXX3/tcSzvvfee\nZVu3aX/zzTcl//TTT5IHDx4sWbcyN8Y6/1kviaLnQm7cuNHjWBKCni8a27zMTz75RHKyZMk8HjN1\n6lTLtp5HU6BAAcl6aSk9H0y3z//2228tjxUO8xS1DRs2WLb1f6em29c///zzkvfs2SPZ/tzVy3m5\nLYuhlyXQc0SjhZ5LqH/2dzO/Xp+j5+rDO2vXrpWs5ykeOHBAsv0z4B9//BH4gYUhvSxdu3btJOul\neuzvr6VLl/b4WClTppQ8ceJEyXqOqV4S0BhjLl++7NuA/YBvFgEAAAAADtwsAgAAAAAcYmJbriAm\nJiboaxkMGjRIstsyGLoFddGiRS37dNkI7ihVqpTkkSNHStY/P12OYIwxJUuWlBzflvXp0qWTvHTp\n0jivr6/tj+tHKl3m07NnT4/HuJVIRYuDBw9K1uU3xhizfft2yfalYyLd77//btnWJZotW7aUnC1b\nNsm1atWSrP92z5w5Y3msPn36SJ49e7bkvXv3ehyLvaSyadOmkuvWrStZl8qmTZtWsv197NatW5J3\n7twpWZcC6nLDYMmaNatk3aZel8r26NHDcs4PP/wgWZfbupWh6mWSjDEmX758kh944AHJqVOn9ni+\n/nupWrWqZd+2bds8nhON9GuH2zJf7777ruShQ4cGekghTb9X2adPuC2R4W25arS/x7lp3ry5ZF3K\nqF8vdeljx44dLefr16VIopeV0+9VX331ld+u8fjjj1u2BwwYIFk/r71ZMk6/7xpjzMKFC+M3OBe3\nb992/UPim0UAAAAAgAM3iwAAAAAAh5ArQ9Vft44dO1ZypkyZPB7frVs3y/bHH38cmIFFkEWLFkl+\n7rnnXI+rVKmS5BUrVsTrmnny5JG8bt06yRkzZpR84sQJybrczBhraRTucPv71WU+9q5c0UB3dZs5\nc6bk++67z3Jc9+7dJUfba8f169ct24kSJfJ43M2bNyVfunRJsi4rGzVqlOUc3S3On3Q3Vl2upjtT\nGmMt99Qlqfr3fezYsUAMMVa6XE6/vzVr1sz1HP3zd/sdxZcuz61cubLkv/76KyDXC0f2n/3//vc/\nyW6d2ylDjR9vSvSMsb7HRXOHcN3F2hhjWrduLVm/9uiuyl26dAn8wEKMfl7p9wf9WcHeHV13rD5+\n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\nq0tevny55DRp0kjWXVKNMeb333+XHA7lP7p86+uvv7bsS548uU+PpcvVjDGmZcuWko8ePSr5zz//\n9Olxo1GRIkUs27ojpO602a5du6CNKaHYn1dDhgyJ85xevXpJ1p01U6ZM6bdxGWNM1apVPf77l19+\nKfn48eN+vWawvfzyy5LvvffOW7H9v+uff/7xeH7GjBklDx8+XLK9tPq+++7zeL4uI9avwxs2bJBs\nf62aOnWqZN2xul+/fh6vkdD++usvy3aVKlUk//DDD5Jz584tWZfn6lJne6mtnjaxe/duyVeuXLn7\nAYewu+lM6m0ZqRbfrqv6NUoL5a6nCSldunSSH3zwQcs+PQVh2bJlQRsTQg/fLAIAAAAAHLhZBAAA\nAAA4RE0Zap06dSRfvXpV8t10+ApHH330UUIPQWzdulWyHtfAgQMl6xInY6ylQa+99loAR+cfuvwm\nadKkln3/+9//JOvSpgMHDkjWJZG6bNcYY+rVqydZl5y9/fbbkseNG3c3w454P/74o2VbL4CuS26i\ngS6jM8aYrFmzxnmOLgPVz9FEiRJ5dU19zt38vDt27Cj5iSeekKy7fBpjzIULF3x+7GBz6348ceJE\ny/aePXsk6+6eXbt2lZwpUybX6+ju05MnT5asy+N110/tySefdH1cPW0gXOhuvrpr7NixYyW7lU5m\nz57dsr1lyxbJ+nf26aefStblqeFOl3d6+7lJl5Hq8+2lovq4+HZdRdz067Xuamyny7h37drl8Zhk\nyZJJvnz5sh9Gh1DEN4sAAAAAAAduFgEAAAAADhFdhqoXQ9eli3px099++y2oY/I3XTKjS7yMMWbd\nunWSZ86cGbQx+WLz5s2SL168KNneXfFuuqIlpDlz5kguV66cZd++ffske7OYddGiRS3bY8aMkaxL\n8YYNGyb5nnvu8Xh8tNMdJI2xlkLqLpSrV68O2pjCie6smRBlu/p1QZcRfvbZZ5bj3nrrraCNyd9m\nz55t2dbl5UOHDvV4jv5d6NdUY4xp1aqV5I0bN/o0Fv06Ynfr1i2fHivU6BJR3X1Xd3186qmnvHos\nPW1Cf+7Qr8l9+/a9q3GGCv0ebO8s6s37c2zlpb6Wnj7zzDOWbTqd+kaXrdeqVcv1uMOHD0vu3r27\n5OLFi0vWHZJ1mTsiC98sAgAAAAAcuFkEAAAAADhwswgAAAAAcIiJbd5JTExM2PWSf/jhhyWvXbtW\n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\n",
            "text/plain": [
              "\u003cFigure size 1600x1000 with 1 Axes\u003e"
            ]
          },
          "metadata": {
            "tags": []
          },
          "output_type": "display_data"
        }
      ],
      "source": [
        "#@test {\"output\": \"ignore\"}\n",
        "\n",
        "import numpy as np\n",
        "from matplotlib import pyplot as plt\n",
        "\n",
        "for images, labels in mnist_train:\n",
        "  sc_logits = cnn_model(images)\n",
        "  break\n",
        "\n",
        "np_images = images.numpy()\n",
        "subclass_images = np.zeros((np_images.shape[1] * 6, NUM_SUBCLASSES,\n",
        "                            np_images.shape[2]))\n",
        "for i in range(NUM_SUBCLASSES):\n",
        "  images_for_subclass = np_images[np.argmax(sc_logits, -1) == i][:6, :, :, 0]\n",
        "  images_for_subclass = images_for_subclass.reshape((-1, np_images.shape[2]))\n",
        "  subclass_images[:images_for_subclass.shape[0], i, :] = images_for_subclass\n",
        "\n",
        "plt.gcf().set_size_inches((16, 10))\n",
        "plt.imshow(subclass_images.reshape(subclass_images.shape[0], -1), cmap='gray')\n",
        "plt.axis('off')"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "x8lzFZjkc5w6"
      },
      "source": [
        "## Student Training"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "9dHkx67WeC1W"
      },
      "source": [
        "### Distill to Student\n",
        "\n",
        "After subclass distillation, the student performs nearly as well as the teacher."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "executionInfo": {
          "elapsed": 41058,
          "status": "ok",
          "timestamp": 1629144168317,
          "user": {
            "displayName": "",
            "photoUrl": "",
            "userId": ""
          },
          "user_tz": 240
        },
        "id": "IiCR0T7JbgiP",
        "outputId": "58ff3eb2-e704-4b60-ad3f-c31929b69706"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Epoch 1: 98.00%\n",
            "Epoch 2: 98.51%\n",
            "Epoch 3: 98.85%\n",
            "Epoch 4: 98.85%\n",
            "Epoch 5: 98.96%\n",
            "Epoch 6: 98.98%\n",
            "Epoch 7: 98.95%\n",
            "Epoch 8: 98.99%\n",
            "Epoch 9: 98.99%\n",
            "Epoch 10: 98.96%\n",
            "Epoch 11: 99.02%\n",
            "Epoch 12: 99.01%\n"
          ]
        }
      ],
      "source": [
        "#@test {\"output\": \"ignore\"}\n",
        "\n",
        "# Hyperparameters for distillation.\n",
        "ALPHA = 0.5\n",
        "DISTILLATION_TEMPERATURE = 4.\n",
        "\n",
        "# Build fully connected student.\n",
        "fc_model = tf.keras.Sequential()\n",
        "fc_model.add(tf.keras.layers.Flatten())\n",
        "fc_model.add(tf.keras.layers.Dense(784, activation='relu'))\n",
        "fc_model.add(tf.keras.layers.Dense(784, activation='relu'))\n",
        "fc_model.add(tf.keras.layers.Dense(NUM_SUBCLASSES))\n",
        "\n",
        "# We copy the student before it is trained and use it below.\n",
        "fc_model_no_distillation = tf.keras.Sequential()\n",
        "fc_model_no_distillation.add(tf.keras.layers.Flatten())\n",
        "fc_model_no_distillation.add(tf.keras.layers.Dense(784, activation='relu'))\n",
        "fc_model_no_distillation.add(tf.keras.layers.Dense(784, activation='relu'))\n",
        "fc_model_no_distillation.add(tf.keras.layers.Dense(NUM_CLASSES))\n",
        "\n",
        "fc_model_conventional_distillation = tf.keras.models.clone_model(\n",
        "    fc_model_no_distillation)\n",
        "\n",
        "def compute_student_loss(images, labels):\n",
        "  \"\"\"Compute subclass distillation student loss for given images and labels.\n",
        "\n",
        "  Args:\n",
        "    images: Tensor representing a batch of images.\n",
        "    labels: Tensor representing a batch of labels.\n",
        "\n",
        "  Returns:\n",
        "    Scalar loss Tensor.\n",
        "  \"\"\"\n",
        "  student_subclass_logits = fc_model(images, training=True)\n",
        "\n",
        "  # Compute distillation loss between student subclass logits and softened\n",
        "  # teacher subclass targets probabilities.\n",
        "  teacher_subclass_logits = cnn_model(images, training=False)\n",
        "  distillation_loss_value = distillation_loss(\n",
        "      teacher_logits=teacher_subclass_logits,\n",
        "      student_logits=student_subclass_logits,\n",
        "      temperature=DISTILLATION_TEMPERATURE)\n",
        "\n",
        "  # Compute cross-entropy loss with hard targets.\n",
        "  student_class_logits = class_logits_from_subclass_logits(\n",
        "      student_subclass_logits, NUM_CLASSES)\n",
        "  cross_entropy_loss_value = tf.reduce_mean(\n",
        "      tf.nn.softmax_cross_entropy_with_logits(labels, student_class_logits))\n",
        "\n",
        "  return (ALPHA * distillation_loss_value +\n",
        "          (1 - ALPHA) * cross_entropy_loss_value)\n",
        "\n",
        "\n",
        "train_and_evaluate(fc_model, compute_student_loss)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "CEKk1VP5eGkO"
      },
      "source": [
        "### Train Student from Scratch\n",
        "\n",
        "When trained from scratch, the student performs substantially worse."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "executionInfo": {
          "elapsed": 23357,
          "status": "ok",
          "timestamp": 1629144191886,
          "user": {
            "displayName": "",
            "photoUrl": "",
            "userId": ""
          },
          "user_tz": 240
        },
        "id": "PInjFtPJiOzf",
        "outputId": "c1414c28-0e8b-438a-e17a-82e77ed0a824"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Epoch 1: 97.87%\n",
            "Epoch 2: 98.13%\n",
            "Epoch 3: 98.24%\n",
            "Epoch 4: 97.71%\n",
            "Epoch 5: 98.25%\n",
            "Epoch 6: 98.58%\n",
            "Epoch 7: 98.32%\n",
            "Epoch 8: 98.39%\n",
            "Epoch 9: 98.47%\n",
            "Epoch 10: 98.51%\n",
            "Epoch 11: 98.54%\n",
            "Epoch 12: 98.53%\n"
          ]
        }
      ],
      "source": [
        "#@test {\"output\": \"ignore\"}\n",
        "\n",
        "def compute_plain_cross_entropy_loss(images, labels):\n",
        "  \"\"\"Compute plain loss for given images and labels.\n",
        "\n",
        "  For fair comparison and convenience, this function also performs a\n",
        "  LogSumExp over subclasses, but does not perform subclass distillation.\n",
        "\n",
        "  Args:\n",
        "    images: Tensor representing a batch of images.\n",
        "    labels: Tensor representing a batch of labels.\n",
        "\n",
        "  Returns:\n",
        "    Scalar loss Tensor.\n",
        "  \"\"\"\n",
        "  student_class_logits = fc_model_no_distillation(images, training=True)\n",
        "  cross_entropy_loss = tf.reduce_mean(\n",
        "      tf.nn.softmax_cross_entropy_with_logits(labels, student_class_logits))\n",
        "  return cross_entropy_loss\n",
        "\n",
        "\n",
        "train_and_evaluate(fc_model_no_distillation, compute_plain_cross_entropy_loss)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "MbbZ_V1MwuOh"
      },
      "source": [
        "### Conventional Distillation from Binary Predictions\n",
        "\n",
        "Baseline: distillation from logits.\n",
        "It performs better than training the student from scratch, but worse than subclass distillation.\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "executionInfo": {
          "elapsed": 37433,
          "status": "ok",
          "timestamp": 1629144280585,
          "user": {
            "displayName": "",
            "photoUrl": "",
            "userId": ""
          },
          "user_tz": 240
        },
        "id": "Q4G56u-Lwrt3",
        "outputId": "cdd78420-d947-4d0e-c5fc-a4e61b8bb276"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Epoch 1: 97.90%\n",
            "Epoch 2: 98.34%\n",
            "Epoch 3: 98.46%\n",
            "Epoch 4: 98.51%\n",
            "Epoch 5: 98.76%\n",
            "Epoch 6: 98.68%\n",
            "Epoch 7: 98.71%\n",
            "Epoch 8: 98.81%\n",
            "Epoch 9: 98.75%\n",
            "Epoch 10: 98.72%\n",
            "Epoch 11: 98.80%\n",
            "Epoch 12: 98.72%\n"
          ]
        }
      ],
      "source": [
        "#@test {\"output\": \"ignore\"}\n",
        "\n",
        "def compute_conventional_distillation_loss(images, labels):\n",
        "  \"\"\"Compute distillation student loss for given images and labels.\n",
        "\n",
        "  Args:\n",
        "    images: Tensor representing a batch of images.\n",
        "    labels: Tensor representing a batch of labels.\n",
        "\n",
        "  Returns:\n",
        "    Scalar loss Tensor.\n",
        "  \"\"\"\n",
        "  student_class_logits = fc_model_conventional_distillation(images,\n",
        "                                                            training=True)\n",
        "\n",
        "  teacher_subclass_logits = cnn_model(images, training=False)\n",
        "\n",
        "  teacher_class_logits = class_logits_from_subclass_logits(\n",
        "      teacher_subclass_logits, NUM_CLASSES)\n",
        "\n",
        "  # Compute distillation loss between student logits and softened\n",
        "  # teacher targets probabilities.\n",
        "  distillation_loss_value = distillation_loss(\n",
        "      teacher_logits=teacher_class_logits,\n",
        "      student_logits=student_class_logits,\n",
        "      temperature=DISTILLATION_TEMPERATURE)\n",
        "  \n",
        "  # Compute cross-entropy loss with hard targets.\n",
        "  cross_entropy_loss_value = tf.reduce_mean(\n",
        "      tf.nn.softmax_cross_entropy_with_logits(labels, student_class_logits))\n",
        "\n",
        "  return (ALPHA * distillation_loss_value +\n",
        "          (1 - ALPHA) * cross_entropy_loss_value)\n",
        "\n",
        "\n",
        "train_and_evaluate(fc_model_conventional_distillation,\n",
        "                   compute_conventional_distillation_loss)"
      ]
    }
  ],
  "metadata": {
    "colab": {
      "collapsed_sections": [],
      "last_runtime": {
        "build_target": "//learning/deepmind/public/tools/ml_python:ml_notebook",
        "kind": "private"
      },
      "name": "Subclass Distillation MNIST.ipynb",
      "provenance": []
    },
    "kernelspec": {
      "display_name": "Python 3",
      "name": "python3"
    },
    "language_info": {
      "name": "python"
    }
  },
  "nbformat": 4,
  "nbformat_minor": 0
}
